package gdf

import (
	"bytes"
	"encoding/hex"
	"errors"
	"gitee.com/liumou_site/logger"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/spf13/cast"
)

var fileTypeMap sync.Map

// bytesToHexString 将字节切片转换为十六进制字符串。
// src: 待转换的字节切片。
// 返回值: 转换后的十六进制字符串。
func bytesToHexString(src []byte) string {
	// 使用bytes.Buffer来高效地构建结果字符串
	res := bytes.Buffer{}
	// 检查src是否为空或长度为0，如果是，则返回空字符串
	if src == nil || len(src) <= 0 {
		return ""
	}
	// temp用于临时存储转换过程中的字节
	temp := make([]byte, 0)
	// 遍历src中的每个字节
	for _, v := range src {
		// 取出字节的最后8位（无符号）
		sub := v & 0xFF
		// 将temp中的字节和当前字节的最后8位一起转换为十六进制字符串
		hv := hex.EncodeToString(append(temp, sub))
		// 如果生成的十六进制字符串长度小于2，说明当前字节是十六进制的低4位
		// 在字符串前补0，确保每个字节都转换为两位十六进制字符串
		if len(hv) < 2 {
			res.WriteString(strconv.FormatInt(int64(0), 10))
		}
		// 将转换后的十六进制字符串追加到结果中
		res.WriteString(hv)
	}
	// 返回构建完成的十六进制字符串
	return res.String()
}

// getFileType 根据文件的二进制数据判断文件的类型。
// 它通过遍历一个全局的文件类型映射表，比较文件的二进制数据的十六进制表示的前缀，
// 与映射表中的键（同样也是十六进制表示的前缀）是否匹配来确定文件类型。
// 参数:
//
//	fSrc []byte: 文件的二进制数据。
//
// 返回值:
//
//	string: 文件的类型，如果找不到匹配的类型，则返回空字符串。
func getFileType(fSrc []byte) string {
	// 初始化文件类型的变量
	var fileType string
	// 将文件的二进制数据转换为十六进制字符串
	fileCode := bytesToHexString(fSrc)

	// 遍历文件类型映射表，寻找匹配的文件类型
	fileTypeMap.Range(func(key, value interface{}) bool {
		// 将映射表中的键值对转换为字符串类型
		k := key.(string)
		v := value.(string)
		// 比较文件的十六进制表示的前缀与映射表中的键是否匹配
		// 匹配条件是：文件的十六进制表示的前缀与键的字符串形式相匹配，
		// 或者键的字符串形式与文件的十六进制表示的前缀相匹配（不区分大小写）
		if strings.HasPrefix(fileCode, strings.ToLower(k)) ||
			strings.HasPrefix(k, strings.ToLower(fileCode)) {
			// 如果找到匹配的类型，则将该类型赋值给文件类型变量，并结束遍历
			fileType = v
			return false
		}
		// 如果当前遍历的项不匹配，则继续遍历下一个项
		return true
	})
	// 返回找到的文件类型，如果未找到，则返回空字符串
	return fileType
}

// SafeMkdir 检查指定的文件夹是否存在，如果不存在，则尝试创建它。
// 参数:
//
//	folder - 需要检查或创建的文件夹路径。
//
// SafeMkdir 不返回任何值，但它可能在尝试创建文件夹时遇到错误。
func SafeMkdir(folder string) {
	// 使用os.Stat检查文件夹是否已存在。
	if _, err := os.Stat(folder); os.IsNotExist(err) {
		// 如果文件夹不存在，尝试使用os.MkdirAll创建文件夹。
		// os.ModePerm设置文件夹权限为对所有用户开放。
		err := os.MkdirAll(folder, os.ModePerm)
		// 如果在创建文件夹过程中遇到错误，则不处理错误直接返回。
		if err != nil {
			return
		}
	}
}

// GetBytesFile 读取指定文件的所有内容到字节切片。
// filename: 需要读取的文件名。
// bufferSize: 读取时使用的缓冲区大小。
// 返回值: 文件的内容作为字节切片，如果读取失败或文件不存在，则返回nil。
func GetBytesFile(filename string, bufferSize int) []byte {
	// 尝试打开文件。
	file, err := os.Open(filename)
	// 如果打开文件失败，记录错误并返回nil。
	if err != nil {
		logger.Error(err)
		return nil
	}
	// 确保在函数返回前关闭文件。
	defer func(file *os.File) {
		// 尝试关闭文件，如果失败，记录错误。
		err := file.Close()
		if err != nil {
			logger.Error(err)
		}
	}(file)

	// 创建一个缓冲区用于读取文件内容。
	buffer := make([]byte, bufferSize)
	// 从文件读取内容到缓冲区。
	_, err = file.Read(buffer)
	// 如果读取失败，记录错误并返回nil。
	if err != nil {
		logger.Error(err)
		return nil
	}
	// 返回读取到的缓冲区内容。
	return buffer
}

// readFile 读取指定文件名的文件内容并返回。
// 如果文件无法打开、读取或关闭，将记录错误并返回nil。
// 参数:
//
//	filename: 需要读取的文件的名称。
//
// 返回值:
//
//	文件的内容作为字节切片返回，如果出现错误则返回nil。
func readFile(filename string) []byte {
	// 尝试打开文件，只读模式。
	tempFile, err := os.OpenFile(filename, os.O_RDONLY, os.ModePerm)
	if err != nil {
		// 如果无法打开文件，记录错误并返回nil。
		logger.Error("Failed to open file " + filename)
		return nil
	}

	// 尝试读取文件的所有内容。
	tempFileBytes, err := io.ReadAll(tempFile)
	if err != nil {
		// 如果无法读取文件内容，记录错误并返回nil。
		logger.Error("Failed to read file data " + filename)
		return nil
	}

	// 确保关闭文件，避免资源泄露。
	err = tempFile.Close()
	if err != nil {
		// 如果无法关闭文件，记录错误并返回nil。
		logger.Error("Failed to close file ", filename)
		return nil
	}

	// 返回读取到的文件内容。
	return tempFileBytes
}

// TestDownload 函数尝试通过HTTP请求下载指定URL的资源，并进行范围请求以测试服务器是否支持多线程下载。
// 参数:
//
//	urlStr - 要下载的资源的URL字符串。
//	perThreadSize - 每个线程负责下载的字节大小。
//
// 返回值:
//
//	int - 资源的总大小。
//	map[int]string - 包含范围请求和对应URL的映射。
//	[]string - 下载的文件名列表。
//	string - 资源的Content-Type。
//	error - 如果发生错误，返回错误信息。
func TestDownload(urlStr string, perThreadSize int) (int, map[int]string, []string, string, error) {
	// 初始化变量length，用于记录已下载文件的长度
	length := 0
	// 初始化rangeMaps，用于存储已尝试的范围请求和对应的URL
	rangeMaps := make(map[int]string)
	// 创建一个新的HTTP请求，旨在获取指定URL的内容
	req, _ := http.NewRequest("GET", urlStr, nil)
	// 添加Range请求头，指定获取内容的范围，这里是前2个字节
	req.Header.Add("Range", "bytes=0-1")
	// 发送请求并获取响应
	req.Header.Add("Range", "bytes=0-1") // 测试下载部分内容
	res, err := http.DefaultClient.Do(req)
	// 初始化contentType变量，用于存储响应的Content-Type
	contentType := ""
	// 初始化rangeIndex，用于标识当前尝试的范围请求序号
	rangeIndex := 1
	// 初始化filenames切片，用于存储已下载的文件名
	var filenames []string
	// 检查请求过程中是否发生错误
	if err != nil {
		// 如果有错误，将当前范围请求和URL记录到rangeMaps中，并返回错误信息
		rangeMaps[rangeIndex] = urlStr
		return length, rangeMaps, filenames, contentType, errors.New("Failed to connect " + urlStr)
	}
	// 检查响应的状态码是否为206，即部分内容响应
	if res.StatusCode != 206 {
		// 如果状态码不是206，将当前范围请求和URL记录到rangeMaps中，并返回错误信息
		rangeMaps[rangeIndex] = urlStr
		return length, rangeMaps, filenames, contentType, errors.New("http status is not equal to 206")
	}
	// 206表示响应成功，仅仅返回部分内容
	contentLength := res.Header.Get("Content-Range")
	contentType = res.Header.Get("Content-Type")
	totalLength, err := strconv.Atoi(strings.Split(contentLength, "/")[1])
	if err != nil {
		return length, rangeMaps, filenames, contentType, errors.New("Can't calculate the content-length form server " + urlStr)
	}
	nowLength := 0 // 记录byte偏移量
	for {
		if nowLength >= totalLength {
			break
		}
		var tempRangeStr string // 记录临时文件名
		// 根据当前长度和每个线程处理的大小计算本次处理的字节范围
		if nowLength+perThreadSize >= totalLength {
			// 如果当前长度加上每个线程处理的大小大于等于总长度，
			// 则本次处理从当前长度开始到总长度的最后一个字节
			tempRangeStr = "bytes=" + strconv.Itoa(nowLength) + "-" + strconv.Itoa(totalLength-1)
			nowLength = totalLength
		} else {
			// 如果当前长度加上每个线程处理的大小小于总长度，
			// 则本次处理从当前长度开始到当前长度加上每个线程处理的大小减一的字节范围
			tempRangeStr = "bytes=" + strconv.Itoa(nowLength) + "-" + strconv.Itoa(nowLength+perThreadSize-1)
			nowLength = nowLength + perThreadSize
		}
		// 将本次处理的字节范围存储到范围映射中
		rangeMaps[rangeIndex] = tempRangeStr
		// 将本次处理的字节范围添加到文件名列表中
		filenames = append(filenames, tempRangeStr)
		// 更新范围索引，为下一次处理准备
		rangeIndex = rangeIndex + 1

	}
	return totalLength, rangeMaps, filenames, contentType, nil
}

// DownloadFileDefault 下载文件并使用URL最后一组作为文件名,例如http://qq/QQ9.6.2.exe则使用QQ9.6.2.exe
// 参数:
//
//	urls - 文件的URL地址
//	folders - 文件保存的目录
//	threadCount - 下载线程的数量
func DownloadFileDefault(urls, folders string, threadCount int) {
	// 记录开始时间，用于计算下载耗时
	now := time.Now()

	// 定义每个线程下载的文件大小为2MB
	oneThreadDownloadSize := 1024 * 1024 * 2 // 一个线程下载文件的大小

	// 初始化下载线程控制器，设定线程数量
	controller := DownloadThreadController{ThreadCount: threadCount}

	// 从URL中提取文件名
	filenamesAll := strings.Split(urls, "/")
	filenames := filenamesAll[len(filenamesAll)-1]

	// 分割文件名以获取文件格式
	filename := strings.Split(filenames, ".")
	fileFormat := filename[len(filename)-1]

	// 构建文件保存的格式后缀
	fileFormats := "." + fileFormat

	// 移除文件格式后缀，得到保存的文件名
	saveName := strings.Replace(filenames, fileFormats, "", 1)

	// 设置下载控制器的文件名、下载地址和保存目录
	controller.DownloadFileName = saveName
	controller.FileUrl = urls
	controller.DownloadFolder = folders

	// 启动下载，使用设定的线程数和下载大小
	controller.downloads(oneThreadDownloadSize)

	// 计算并输出下载使用的时间
	useTime := cast.ToString(time.Since(now))

	// 输出下载耗时信息
	logger.Debug("Spend time: ", useTime)
}

// DownloadFile 下载文件函数
// 参数:
//
//	urls: 文件的URL地址
//	file: 目标保存文件的路径
//	threadCount: 下载线程的数量
//
// 该函数通过多个线程并发下载文件，以提高下载速度
func DownloadFile(urls, file string, threadCount int) {
	// 获取文件保存路径的目录部分
	folders := filepath.Dir(file)
	// 记录开始下载的时间
	now := time.Now()
	// 定义一个线程下载文件的大小为2MB
	oneThreadDownloadSize := 1024 * 1024 * 2 // 一个线程下载文件的大小
	// 初始化下载线程控制器，设置线程数量
	controller := DownloadThreadController{ThreadCount: threadCount}
	// 获取文件名
	saveName := filepath.Base(file)
	// 设置控制器的下载文件名、文件URL和下载目录
	controller.DownloadFileName = saveName
	controller.FileUrl = urls
	controller.DownloadFolder = folders

	// 使用控制器开始下载文件
	controller.downloads(oneThreadDownloadSize)
	// 计算下载花费的时间
	useTime := cast.ToString(time.Since(now))
	// 记录日志，输出下载花费的时间
	logger.Debug("Spend time: ", useTime)
}

// put 方法用于向下载线程控制器添加一个新的下载任务。
// 它通过 WaitGroup 管理并发，通过 TaskCount 控制并发数量，通过 TaskQueue 实现任务队列。
// 当添加任务时，如果发生panic，会通过recover捕获并记录错误。
func (controller *DownloadThreadController) put(task DownloadTask) {
	// 使用defer和recover捕获并处理panic，确保任务添加过程中出现的异常不会导致程序崩溃。
	defer func() {
		err := recover()
		if err != nil {
			// 记录panic信息，便于问题追踪。
			logger.Error("Channel closed", err)
		}
	}()

	// 告诉WaitGroup有一个新的任务将要开始，用于管理并发。
	controller.WaitGroup.Add(1)
	// 向TaskCount通道发送信号，表示有一个新任务加入，用于控制并发数量。
	controller.TaskCount <- 1
	// 将新任务放入任务队列，等待处理。
	controller.TaskQueue <- task
	// 注释掉的代码可能是预留的自定义任务处理逻辑，根据实际需求进行实现。
	//go task.customFunc(task.paramsInfo)
}

// downloadFiles 启动一个下载任务来下载指定的文件。
// paramsInfo 包含下载所需的参数信息。
func (controller *DownloadThreadController) downloadFiles(paramsInfo interface{}) {
	// 任务完成时减少等待组的计数器
	defer func() {
		controller.WaitGroup.Done() // 下载任务完成，协程结束
	}()

	// 处理下载过程中的panic情况，确保panic不会导致整个程序崩溃
	defer func() {
		err := recover() //内置函数，可以捕捉到函数异常
		if err != nil {
			logger.Error("Failed to download file：", err)
		}
	}()

	// 断言paramsInfo为DownloadFileParams类型，并处理下载逻辑
	switch paramsInfo.(type) {
	case DownloadFileParams:
		params := paramsInfo.(DownloadFileParams)

		// 默认设置下载成功标志为false，只有在下载完全成功时才设置为true
		params.Successes = false

		// 在这个defer中处理可能的panic，确保即使发生错误也能正确处理成功标志和日志
		defer func() {
			err := recover() //内置函数，可以捕捉到函数异常
			if err != nil {
				logger.Error("Failed to download file：", err)
				params.Successes = false
			}
		}()

		// 下载逻辑开始
		urlStr := params.UrlStr
		rangeStr := params.RangeStr
		tempFilename := params.TempFilename

		// 尝试删除已存在的临时文件，为新下载的文件做准备
		err := os.Remove(tempFilename)
		if err != nil {
			logger.Error("Failed to remove file " + tempFilename)
			return
		} // 删除已有的文件, 避免下载的数据被污染

		// 创建HTTP请求，指定下载的URL和Range头信息
		// 发起文件下载请求
		req, _ := http.NewRequest("GET", urlStr, nil)
		req.Header.Add("Range", rangeStr)      // 测试下载部分内容
		res, err := http.DefaultClient.Do(req) // 发出下载请求，等待回应

		// 处理请求错误或非206状态码的情况
		if err != nil {
			logger.Error("Failed to connect " + urlStr)
			params.Successes = false // 无法连接, 标记下载失败
		} else if res.StatusCode != 206 {
			params.Successes = false
		} else {
			// 打开或创建临时文件，准备写入下载的内容
			// 打开文件，写入文件
			fileObj, err := os.OpenFile(tempFilename, os.O_RDONLY|os.O_CREATE|os.O_APPEND, 0666)
			if err != nil {
				logger.Error("Failed to open file " + tempFilename)
				params.Successes = false // 无法打开文件, 标记下载失败
			} else {
				defer func(fileObj *os.File) {
					err := fileObj.Close()
					if err != nil {
						logger.Error("Failed to close file " + tempFilename)
					}
				}(fileObj) // 确保文件在函数返回前关闭

				// 读取响应体的内容
				body, err := io.ReadAll(res.Body) // 读取响应体的所有内容
				if err != nil {
					logger.Error("Failed to read response body.")
					params.Successes = false
				} else {
					defer func(Body io.ReadCloser) {
						err := Body.Close()
						if err != nil {
							logger.Error("Failed to close response body.")
						}
					}(res.Body) // 确保响应体在函数返回前关闭

					// 将读取到的内容写入到临时文件中
					_, err = fileObj.Write(body)
					if err != nil {
						logger.Error(err)
						return
					}
					params.Successes = true // 下载成功
				}
			}
		}

		// 将下载结果发送到结果信息通道
		controller.DownloadResultInfoChan <- params // 将下载结果传入
	}
}

// Run 方法启动下载线程控制器，负责调度和执行下载任务。
// 它遍历控制器中的所有下载范围字符串，并为每个范围创建一个下载任务。
// 控制器根据配置决定是保存文件到指定名称还是使用临时文件名。
// 此方法通过向任务队列中添加任务来启动下载过程。
func (controller *DownloadThreadController) Run() {
	// 遍历所有下载范围
	// 只需要将待下载的请求发送一次即可，成功了会直接剔除，不成功则由接收方重试
	for rangeIndex, rangeStr := range controller.RangeStrs {
		// 初始化下载参数，包括URL、范围、临时文件名、范围索引和初始的成功状态
		params := DownloadFileParams{UrlStr: controller.FileUrl, RangeStr: rangeStr, TempFilename: controller.DownloadFolder + "/" + rangeStr, RangeIndex: rangeIndex, Successes: true}
		// 根据控制器的保存设置，确定最终的文件名
		if controller.Save {
			params.TempFilename = controller.DownloadFileName
		} else {
			params.TempFilename = controller.Filenames[rangeIndex]
		}
		// 输出调试信息，显示当前任务的参数
		logger.Debug(params)
		// 创建下载任务，包含下载函数和参数
		task := DownloadTask{controller.downloadFiles, params}
		// 将任务放入队列，等待执行
		controller.put(task) // 若通道满了会阻塞，等待空闲时再下载
	}
}

// getSuffix 根据内容类型返回对应的文件后缀。
// 如果内容类型不在已知类型列表中，则返回空字符串。
// 参数:
//
//	contentType - 文件的内容类型。
//
// 返回值:
//
//	文件的后缀名，如果无法确定则为空字符串。
func (controller *DownloadThreadController) getSuffix(contentType string) string {
	// 初始化后缀名为空字符串
	suffix := ""

	// 定义一个映射，用于内容类型到文件后缀名的映射
	contentTypes := map[string]string{
		"image/gif":                    "gif",
		"image/jpeg":                   "jpg",
		"application/x-img":            "img",
		"image/png":                    "png",
		"application/json":             "json",
		"application/pdf":              "pdf",
		"application/msword":           "word",
		"application/octet-stream":     "rar",
		"application/x-zip-compressed": "zip",
		"application/x-msdownload":     "exe",
		"video/mpeg4":                  "mp4",
		"video/avi":                    "avi",
		"audio/mp3":                    "mp3",
		"text/css":                     "css",
		"application/x-javascript":     "js",
		"application/vnd.android.package-archive": "apk",
	}

	// 遍历内容类型映射，寻找匹配的内容类型
	for key, value := range contentTypes {
		// 如果当前内容类型包含在给定的contentType中，则找到匹配项
		if strings.Contains(contentType, key) {
			// 将匹配到的后缀名赋值给suffix，并结束循环
			suffix = value
			break
		}
	}

	// 返回找到的后缀名，如果未找到则返回空字符串
	return suffix
}

// combineFiles 合并下载目录中的所有分片文件到指定的文件名。
// 如果合并成功，它会删除所有的分片文件。如果出现任何错误，它将记录错误并返回false。
func (controller *DownloadThreadController) combineFiles(filename string) bool {
	// 先判断是否已经存在目标文件
	if _, err := os.Stat(controller.DownloadFolder + "/" + filename); err == nil {
		// 尝试删除已存在的目标文件，以便重新创建一个新的合并文件
		err := os.Remove(controller.DownloadFolder + "/" + filename)
		if err != nil {
			logger.Error(err.Error())
			return false
		}
	}
	// 打开或创建目标文件，以追加模式写入来自分片文件的内容。
	goalFile, err := os.OpenFile(controller.DownloadFolder+"/"+filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, os.ModePerm)
	if err != nil {
		logger.Error("Failed to open file ")
		return false
	}

	// 遍历所有分片文件名，读取每个分片的内容并写入目标文件。
	// 正确的话应按照初始计算的文件名顺序合并，并且无缺失
	for _, value := range controller.Filenames {
		retryTime := 3
		var tempFileBytes []byte
		// 尝试多次读取分片文件，以应对读取失败的情况。
		for retryTime > 0 {
			tempFileBytes = readFile(value)
			time.Sleep(100) // 休眠100毫秒，看看是不是文件加载错误
			if tempFileBytes != nil {
				break
			}
			retryTime = retryTime - 1
		}

		// 将分片文件的内容写入目标文件。
		_, err = goalFile.Write(tempFileBytes)
		if err != nil {
			logger.Error(err)
			return false
		}

		// 删除已处理的分片文件。
		err = os.Remove(value)
		if err != nil {
			logger.Error(err)
			return false
		}
	}

	// 关闭目标文件。
	err = goalFile.Close()
	if err != nil {
		logger.Error(err)
		return false
	}

	// 合并操作成功。
	return true
}
